{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Example: Evolutive equilibrium calculations\n", "\n", "This notebook will demonstrate the core use-case for FreeGSNKE: simulating the (time-dependent) evolution of Grad-Shafranov (GS) equilibria. In particular we will simulate a **vertical displacement event (VDE)**.\n", "\n", "To do this, we need to:\n", "- build the tokamak machine.\n", "- instatiate a GS equilibrium (to be used as an initial condition for the evolutive solver).\n", "- calculate a vertical instability growth rate for this equilibrium and carry out passive structure mode removal via a normal mode decomposition (i.e. removing modes that have little effect on the evolution). More details on this in a later notebook. \n", "- define time-dependent coil voltages, plasma current density profile parameters, and plasma resistivity.\n", "- evolve the active coil currents, the total plasma current, and the equilbirium using these profile parameters and voltages by solving the circuit equations alongside the GS equation.\n", "\n", "Refer to the paper by [Amorisco et al. (2024)](https://pubs.aip.org/aip/pop/article/31/4/042517/3286904/FreeGSNKE-A-Python-based-dynamic-free-boundary) for more details." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Import packages" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import numpy as np\n", "import matplotlib.pyplot as plt\n", "from IPython.display import display, clear_output\n", "import pickle" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Build the machine" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# build machine\n", "from freegsnke import build_machine\n", "tokamak = build_machine.tokamak(\n", " active_coils_path=f\"../machine_configs/MAST-U/MAST-U_like_active_coils.pickle\",\n", " passive_coils_path=f\"../machine_configs/MAST-U/MAST-U_like_passive_coils.pickle\",\n", " limiter_path=f\"../machine_configs/MAST-U/MAST-U_like_limiter.pickle\",\n", " wall_path=f\"../machine_configs/MAST-U/MAST-U_like_wall.pickle\",\n", ")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Diverted plasma example" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Instantiate an equilibrium" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from freegsnke import equilibrium_update\n", "\n", "eq = equilibrium_update.Equilibrium(\n", " tokamak=tokamak,\n", " Rmin=0.1, Rmax=2.0, # Radial range\n", " Zmin=-2.2, Zmax=2.2, # Vertical range\n", " nx=65, # Number of grid points in the radial direction (needs to be of the form (2**n + 1) with n being an integer)\n", " ny=129, # Number of grid points in the vertical direction (needs to be of the form (2**n + 1) with n being an integer)\n", " # psi=plasma_psi\n", ") " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Instatiate a profile object" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from freegsnke.jtor_update import ConstrainPaxisIp\n", "\n", "profiles = ConstrainPaxisIp(\n", " eq=eq, # equilibrium object\n", " paxis=8.1e3, # profile object\n", " Ip=6.2e5, # plasma current\n", " fvac=0.5, # fvac = rB_{tor}\n", " alpha_m=1.8, # profile function parameter\n", " alpha_n=1.2 # profile function parameter\n", ")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Set coil currents\n", "Here we set coil currents that create a diverted plasma (as seen in prior notebooks). " ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "with open('data/simple_diverted_currents_PaxisIp.pk', 'rb') as f:\n", " current_values = pickle.load(f)\n", "\n", "for key in current_values.keys():\n", " eq.tokamak.set_coil_current(coil_label=key, current_value=current_values[key])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Instatiate the solver\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from freegsnke import GSstaticsolver\n", "GSStaticSolver = GSstaticsolver.NKGSsolver(eq) " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Call forward solver to find equilibrium " ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "GSStaticSolver.solve(eq=eq, \n", " profiles=profiles, \n", " constrain=None, \n", " target_relative_tolerance=1e-8,\n", " verbose=0\n", " )" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Plot the initial equilibrium " ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "fig1, ax1 = plt.subplots(1, 1, figsize=(4, 8), dpi=80)\n", "ax1.grid(True, which='both')\n", "eq.plot(axis=ax1, show=False)\n", "eq.tokamak.plot(axis=ax1, show=False)\n", "ax1.set_xlim(0.1, 2.15)\n", "ax1.set_ylim(-2.25, 2.25)\n", "plt.tight_layout()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Time evolution\n", "\n", "We are now ready to solve the forward time-evolutive problem.\n", "\n", "#### Initialise nonlinear (evolutive) solver\n", "To start, we need to initialise the evolutive solver object, `freegsnke.nonlinear_solve.nl_solver`.\n", "\n", "The time-evolutive solver requires:\n", "- `eq`: an equilibrium to inform the solver of the machine and domain properties.\n", "- `profiles`: defined above.\n", "- `full_timestep`: the time step by which time is advanced at every call of the stepper (this can be modified later depending on the growth rate of the equilibrium). An appropriate time step can also be set based on the growth rate calculation. Use `automatic_timestep` to set the time step in units of the (inverse of the) growth rate.\n", "- `plasma_resistivity`: resistivity of the plasma (which here is assumed to be constant during the time evolution but can be made time-dependent if known).\n", "- `min_dIy_dI`: threshold value below which passive structure normal modes are dropped. Modes with norm(d(Iy)/dI)<`min_dIy_dI` are dropped, which filters out modes that do not actually couple with the plasma.\n", "- `max_mode_frequency`: threshold value for characteristic frequencies above which passive structure normal modes are dropped (i.e. the fast modes).\n", "\n", "Other customisable inputs are available, do see the documentation or the later notebook on \"Growth Rates\" for more details. For example, one may explicitly set your own resistance and inductance matries for the tokamak, rather than the geometrical value calculated internally in FreeGSNKE.\n", "\n", "The solver can be used on different equilibria and/or profiles, but these need to have the same machine, domain, and limiter as the one used at the time of the solver instantiation. For different machines, a new time-evolutive solver should be created.\n", "\n", "The input equilibrium and profile functions are also used as the expansion point around which the dynamics are linearised (when using the linear solver or calculating linear growth rates). " ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "from freegsnke import nonlinear_solve\n", "\n", "stepping = nonlinear_solve.nl_solver(\n", " eq=eq, \n", " profiles=profiles, \n", " GSStaticSolver=GSStaticSolver,\n", " full_timestep=5e-4, \n", " plasma_resistivity=1e-6,\n", " max_mode_frequency=10**2.5,\n", ")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Set active coil voltages\n", "\n", "In this example, we will evolve a plasma in absence of any control policy or current drive.\n", "\n", "Just as an example, the following calculates active voltages to be applied to the poloidal field coils (and Solenoid) using $V = RI$, with current values as defined by the initial equilibrium (i.e. we have constant voltages).\n", "\n", "In most FreeGSNKE use cases, these active voltages will be determined by a control policy." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "voltages = (stepping.vessel_currents_vec*stepping.evol_metal_curr.coil_resist)[:stepping.evol_metal_curr.n_active_coils] " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "To start, the solver is prepared by setting the initial conditions." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "stepping.initialize_from_ICs(eq, profiles)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Set time steps and storage lists\n", "Now we set the total number of time steps we want to simulate and initialise some variables for logging the evolution." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# number of time steps to simulate\n", "max_count = 50\n", "\n", "# initialising some variables for iteration and logging\n", "counter = 0\n", "t = 0\n", "\n", "history_times = [t]\n", "history_currents = [stepping.currents_vec]\n", "history_equilibria = [stepping.eq1.create_auxiliary_equilibrium()]\n", "history_o_points = [stepping.eq1.opt[0]]\n", "history_elongation = [stepping.eq1.geometricElongation()]\n", "history_triangularity = [stepping.eq1.triangularity()]\n", "history_squareness = [stepping.eq1.squareness()[1]]\n", "history_area = [stepping.eq1.separatrix_area()]\n", "history_length = [stepping.eq1.separatrix_length()]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Set time-dependent active coil voltages, profile parameters, and plasma resistivity\n", "\n", "To simulate the VDE in the example, we will evolve a plasma in absence of any control policy or current drive. The following calculates active voltages to be applied to the poloidal field coils (and Solenoid) using $V = RI$, with current values as defined by the initial equilibrium (i.e. we have constant voltages over time). For more realistic plasma simulation, these active voltages would be truly time-dependent and determined by a control policy.\n", "\n", "In this example, the profile parameters and the plasma resistivity will also remain constant over time. In the next example, we will show how to vary these." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "voltages = (stepping.vessel_currents_vec*stepping.evol_metal_curr.coil_resist)[:stepping.evol_metal_curr.n_active_coils] " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Call the solver (linear)\n", "Finally, we call the time-evolutive solver with `stepping.nlstepper()` sequentially until we reach the preset end time.\n", "\n", "The following demonstrates a solely linear evolution of the plasma by setting `linear_only=True`. This makes use of the Jacobians calculated when the nonlinear solver object was initialised. Note that the linear solution will only hold for equilibrium \"close to\" that which was used to initialise the nonlinear solver object. " ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# initialise the solver with the initial equilibrium/profiles\n", "stepping.initialize_from_ICs(eq, profiles)\n", "\n", "# loop over time steps\n", "while counter